import os
import socket
import threading
import jsonpickle

from Bases import SyncThread
from osc_base import OSCBaseObject
from category import Category

from Universe import Universe

from Devices import Device
from Attributes import Attribute

class DeviceSystem(OSCBaseObject):
    _saved_class_name = 'DeviceSystem'
    _saved_child_objects = ['universes', 'Categories', 'GlobalAttributes', 'DefinedDevices', 'PatchedDevices']
    _saved_child_classes = [Universe, Category, Attribute.GlobalAttribute, 
                            Device.DefinedDevice, Device.PatchedDevice]
    def __init__(self, **kwargs):
        self.MainController = kwargs.get('MainController')
        self.comm = self.MainController.comm
        #self.osc_parent_node = self.comm.osc_io.root_node
        #self.osc_address = 'Devices'
        self.Categories = {}
        self.root_category = Category(name='root', id='root')
        for key in ['device', 'attribute', 'effects']:
            category = self.root_category.add_subcategory(name=key, id=key)
            self.Categories.update({key:category.subcategories})
        super(DeviceSystem, self).__init__(**kwargs)
        self.osc_dump_threads = set()
        self.osc_child_nodes = {}
        for name in ['Devices']:
            self.osc_child_nodes.update({name:self.osc_node.add_new_node(name=name)})
        self.add_osc_handler(callbacks={'request-dump':self.on_osc_request_dump, 
                                        'dump-response':self.on_osc_dump_response, 
                                        'child-added':self.on_osc_child_added})
        self.osc_universe_node = self.comm.osc_io.root_node.add_new_node(name='OSCtoOLA')
        #self.osc_universe_node.addCallback('OSCtoOLA/universes-info', self.on_osc_universes_info)        
        
        #self.DeviceTypes = DeviceTypes().get_types()
        self.universes = {}
        self.GlobalAttributes = {}
        self.DefinedDevices = {}
        self.PatchedDevices = {}
        self.DevicesByNum = {}
        self.patch_by_chan = {}
        self.patch_by_attrib = {}
        
        self.category_type_map = {'device':[self.DefinedDevices, self.PatchedDevices], 
                                  'attribute':[self.GlobalAttributes, self.patch_by_attrib]}
    
        self.register_signal('child_added', 'new_category', 'new_universe', 'new_definition', 
                             'new_device', 'new_attribute', 'del_attribute',
                             'del_device', 'patch_change')
        
        #self.connect('new_universe', self.comm.on_new_universe)
        self.comm.connect('state_changed', self.on_comm_state)
        #self.connect('new_category', self._on_new_category)
        #self.add_universe(name='A', ola_universe_id=1)
        self.signal_dict_map = {'new_category':'Categories', 'new_universe':'universes', 
                                'new_definition':'DefinedDevices', 'new_device':'PatchedDevices'}
        for sig in self.signal_dict_map.iterkeys():
            self.connect(sig, self.on_signals_to_osc)
            
            
    def on_osc_request_dump(self, **kwargs):
        if self.osc_node.oscMaster:
            values = kwargs.get('values')
            client = kwargs.get('client')
            print 'client = ', client
            if len(values):
                for key in values:
                    val = getattr(self, key)
                    if key == 'Categories':
                        s = self.to_json(saved_child_objects=[key], indent=None)
                        self.osc_node.send_message(address='dump-response', value=s, client=client)
                    else:
                        for ckey, cval in val.iteritems():
                            s = cval.to_json(indent=None)
                            d = jsonpickle.decode(s)
                            s = jsonpickle.encode({'saved_children':{key:{d['attrs']['id']:d}}})
                            self.osc_node.send_message(address='dump-response', value=s, client=client)
                    #s = self.to_json(saved_child_objects=[key], indent=None)
                    #self.osc_node.send_message(address='dump-response', value=s, client=client)
            else:
                s = self.to_json(indent=None)
                self.osc_node.send_message(address='dump-response', value=s, client=client)
        
    def on_osc_dump_response(self, **kwargs):
        t = SyncThread(callback=self.on_osc_dump_thread_end)
        self.osc_dump_threads.add(t)
        t.start()
        #print 'preload = ', self.osc_dump_threads
        self.from_json(string=kwargs.get('values')[0])
        
    def on_osc_dump_thread_end(self, obj):
        self.osc_dump_threads.discard(obj)
        #print 'thread end', obj
        #print 'all threads', self.osc_dump_threads
    
    def on_signals_to_osc(self, **kwargs):
        name = kwargs.get('signal_name')
        id = kwargs.get('id')
        key = self.signal_dict_map.get(name)
        if key and not self.osc_dump_threads:
            obj = getattr(self, key).get(id)
            if obj:
                s = obj.to_json(indent=None)
                self.osc_node.send_message(address='child-added', value=[key, s])
        #print 'loading from osc = ', self.osc_dump_threads
                
    def on_osc_child_added(self, **kwargs):
        values = kwargs.get('values')
        key = values[0]
        d = jsonpickle.decode(values[1])
        s = jsonpickle.encode({'saved_children':{key:{d['attrs']['id']:d}}})
        self.from_json(saved_child_objects=[key], string=s)
        
    def request_osc_updates(self, **kwargs):
        self.osc_node.send_message(address='request-dump', value=list(self.saved_child_objects), to_master=True)
        
    def on_comm_state(self, **kwargs):
        if kwargs.get('state'):
            self.osc_universe_node.send_message(root_address='OSCtoOLA-'+socket.gethostname(), 
                                                address='request-universes')
            
    def on_osc_universes_info(self, **kwargs):
        values = kwargs.get('values')
        d = jsonpickle.decode(values[0])
        print 'univ info from osc:', d
    
#    def _on_new_category(self, **kwargs):
#        category = kwargs.get('category')
#        category.connect('obj_update_needed', self._on_category_obj_request)
#        category._check_member_obj()
#    
#    def _on_category_obj_request(self, **kwargs):
#        id = kwargs.get('category_id')
#        obj_id = kwargs.get('obj_id')
#        callback = kwargs.get('callback')
#        for cTypeKey, cTypeVal in self.Categories.iteritems():
#            for catKey, catVal in cTypeVal.iteritems():
#                if catVal.id == id:
#                    category = catVal
#                    category_type = cTypeKey
#        obj = None
#        for obj_dict in self.category_type_map[category_type]:
#            if obj_id in obj_dict:
#                obj = obj_dict[obj_id]
#        callback(obj=obj, id=obj_id)
#    
#    def get_stuff(self):
#        return 'stuff'
    
    def add_universe(self, **kwargs):
        '''
        :Parameters:
            'id' : int, id for the universe, does not auto-assign UUID
            'name' : str
            'from_xml' : xml string which, if specified, the xml will be loaded
        '''
        #id = kwargs.get('id', self.find_next_universe_id())
        name = kwargs.get('name')
        new_kwargs = kwargs.copy()
        new_kwargs.update({'comm':self.comm, 'osc_address':name, 
                           'osc_parent_node':self.osc_universe_node})
        universe = Universe(**new_kwargs)
        self.universes.update({universe.id:universe})
        self.patch_by_chan.update({universe.id:{}})
        self.emit('new_universe', id=universe.id, universe=universe)
        self.emit('child_added')
        return universe
        
    def find_next_universe_id(self):
        keys = self.universes.keys()
        if len(keys) == 0:
            return 1
        return max(keys) + 1
        
    def add_category(self, **kwargs):
        cat_type = kwargs.get('type')
        name = kwargs.get('name')
        if cat_type in self.Categories:
            category = self.root_category.subcategories[cat_type].add_subcategory(**kwargs)
            #self.Categories[cat_type].update({category.id:category})
            self.emit('new_category', type=cat_type, name=category.name, id=category.id, category=category)
            self.emit('child_added')
            print 'new_category:', cat_type, category.name
        
    def add_global_attribute(self, **kwargs):
        '''
        :Parameters:
           'name' : default is empty
            'id' : Universal ID.  Default is None (which assigns a new uuid)
            'type' : class specification (in progress)
            'from_xml' : xml string which, if specified, the xml will be loaded
                into the attribute.  default is None. 
        '''
        kwargs.setdefault('root_category', self.root_category.subcategories['attribute'])
        attribute = Attribute.GlobalAttribute(**kwargs)
        attribute.connect('id_changed', self.on_global_attrib_id_changed)
        self.GlobalAttributes.update({attribute.id:attribute})
        self.emit('new_attribute', type='GlobalAttribute', id=attribute.id, name=attribute.name)
        self.emit('child_added')
        return attribute
        
    def del_global_attribute(self, **kwargs):
        attribute = kwargs.get('attribute')
        attribute.unlink()
        del self.GlobalAttributes[attribute.id]
        self.emit('del_attribute', type='GlobalAttribute', id=attribute.id)
        
    def on_global_attrib_id_changed(self, **kwargs):
        pass
        
    def add_definition(self, **kwargs):
        '''
        :Parameters:
            'name' : default is empty
            'id' : Universal ID.  Default is None (which assigns a new uuid)
            'description' : default is empty
            'manufacturer' : default is empty
            'channel_count' : int, maximum number of channels for the device.
                default is 0
            'type' : device type name. default is 'NormalDevice'
            'from_xml' : xml string which, if specified, the xml will be loaded
                into the device.  default is None.
        '''
        
        kwargs.update({'root_category':self.root_category.subcategories['device']})
        kwargs.update({'GlobalAttributes':self.GlobalAttributes})
        device = Device.DefinedDevice(**kwargs)
        device.connect('new_attribute', self._on_new_defined_attribute)
        self.DefinedDevices.update({device.id:device})
        self.emit('new_device', base=device.base_class, type=device.saved_class_name, id=device.id)
        self.emit('child_added')
        return device
        
    def del_definition(self, **kwargs):
        device = kwargs.get('device')
        device.unlink()
        del self.DefinedDevices[device.id]
        self.emit('del_device', type='DefinedDevice', id=device.id)
        
    def patch_device(self, definition, **kwargs):
        '''Takes given device definition and make a patched
        device from it.
        
        :Parameters:
            'definition' : DefinedDevice to use
            'name' : Default is None (which uses definition name)
            'id' : Universal ID.  Default is None (which assigns a new uuid)
            'device_num' : int, index of the device to be used in the UI (for organization)
            'universe_id' : id of the DMX universe this device belongs to
            'start_chan' : int, starting DMX channel
            'count' : Number of devices to patch. Default is 1
        '''
        count = int(kwargs.get('count', 1))
        base_start = int(kwargs.get('start_chan', 1))
        start = base_start
        new_kwargs = kwargs.copy()
        new_kwargs.update({'osc_parent_node':self.osc_child_nodes['Devices']})
        new_kwargs.update({'root_category':self.root_category.subcategories['device']})
        new_kwargs.update({'definition':definition})
        for x in range(count):
            #print 'start=', start
            valid = self.test_patch(device=definition, universe_id=kwargs.get('universe_id'), 
                                    start_chan=start)
            if not valid:
                return
            device_num = self._find_max_device_num() + 1
            new_kwargs.update({'start_chan':start, 'device_num':device_num, 'osc_address':device_num})
            device = Device.PatchedDevice(**new_kwargs)
            device.connect('device_num_update', self.on_device_num_update)
            self.PatchedDevices.update({device.id:device})
            self.DevicesByNum.update({device.device_num:device})
            self._update_attributes(device=device)
            self.emit('new_device', base='PatchedDevice', id=device.id, device=device)
            self.emit('child_added')
            start += definition.channel_count
        
    def repatch_device(self, **kwargs):
        '''Repatch an existing device
        :Parameters:
            'device' : 
            'universe_id' : new universe to patch into, or if not given,
                uses the current universe for the device
            'start_chan' : new starting DMX channel
        '''
        device = kwargs.get('device')
        univ_id = kwargs.get('universe_id', device.universe_id)
        start = kwargs.get('start_chan')
        old_univ = device.universe_id
        old_start = device.start_chan
        if not self.test_patch(**kwargs):
            return
        for attrib in device.Attributes.itervalues():
            attrib.disconnect(callback=self.universes[old_univ].on_attribute_value_changed)
            del self.patch_by_chan[old_univ][attrib.chan]
        device.repatch(**kwargs)
        self._update_attributes(device=device)
        self.emit('patch_change', device=device, old_universe=old_univ, 
                  new_universe=univ_id, old_start=old_start, new_start=start)

    def test_patch(self, **kwargs):
        device = kwargs.get('device')
        univ_id = kwargs.get('universe_id')
        start = kwargs.get('start_chan')
        count = device.channel_count
        if start + count - 1 > 512:
            return False
        for x in range(start, count+start):
            attrib = self.patch_by_chan[univ_id].get(x)
            if attrib and attrib != device.attr_by_chan.get(x):
                return False
        return True
        
            
    def on_device_num_update(self, **kwargs):
        device = kwargs.get('device')
        value = kwargs.get('value')
        old = kwargs.get('old')
        old_dev = self.DevicesByNum.get(value)
        if old_dev and old_dev != device:
            print 'resetting, old=%s, value=%s' % (old, value)
            device._device_num = old
            device.device_num = old
        elif old != value:
            del self.DevicesByNum[old]
            self.DevicesByNum[value] = device
            print 'changed, old=%s, value=%s' % (old, value)
            
    def on_check_device_num(self, **kwargs):
        device = kwargs.get('device')
        
    def _find_max_device_num(self):
        if len(self.DevicesByNum) == 0:
            return 0
        return max(self.DevicesByNum.keys())
        
    def _on_new_defined_attribute(self, **kwargs):
        devID = kwargs.get('device_id')
        attrID = kwargs.get('attribute_id')
        
    def on_attribute_value_changed(self, **kwargs):
        #print 'ds: ', kwargs
        pass
        
    def _update_attributes(self, **kwargs):
        kwargs.setdefault('device', None)
        kwargs.setdefault('device_id', None)
        kwargs.setdefault('attribute', None)
        device = kwargs.get('device')
        device_id = kwargs.get('device_id')
        attribute = kwargs.get('attribute')
        if attribute is not None:
            return
        if device_id is not None:
            device = self.PatchedDevices[device_id]
        if device is not None:
            for key, val in device.Attributes.iteritems():
                self.patch_by_attrib.update({val.id:val})
                if val.chan is not None:
                    self.patch_by_chan[val.universe_id].update({val.chan:val})
                    univ = self.universes[val.universe_id]
                    univ.on_attribute_value_changed(attribute=val, value=val.value)
                    val.connect('value_changed', self.on_attribute_value_changed)
                    val.connect('value_changed', univ.on_attribute_value_changed)
                
    def on_new_ola_universe(self, **kwargs):
        univ = kwargs.get('ola_universe')
        for u in self.universes.itervalues():
            if u.ola_universe_id == univ.id:
                univ.Universe = u
        
    def load_globals(self, **kwargs):
        kwargs.setdefault('saved_child_objects', ['Categories', 'universes', 'GlobalAttributes', 'DefinedDevices'])
        self.from_json(**kwargs)
        
    def save_globals(self, **kwargs):
        return self._get_saved_attr(saved_child_objects=['Categories', 'universes', 'GlobalAttributes', 'DefinedDevices'])
    
    def from_json(self, **kwargs):
        filename = kwargs.get('filename', None)
        string = kwargs.get('string', '')
        if string == '' and filename is not None and os.path.exists(filename):
            file = open(filename, 'r')
            for line in file:
                string += line
            file.close()
        if string != '':
            d = jsonpickle.decode(string)
            parse_keys = kwargs.get('saved_child_objects', ['Categories', 'universes', 'GlobalAttributes', 'DefinedDevices', 'PatchedDevices'])
            for pKey in parse_keys:
                if pKey in d['saved_children']:
                    for key, val in d['saved_children'][pKey].iteritems():
                        if pKey == 'universes' and key not in self.universes:
                            universe = Universe(deserialize=val, osc_address=val['attrs']['id'], osc_parent_node=self.osc_universe_node)
                            #universe.connect('value_update', self.comm.dmx_io['NetlinxDMX'].sendDMXValue)
                            self.universes.update({universe.id:universe})
                            self.patch_by_chan.update({universe.id:{}})
                            self.emit('new_universe', id=universe.id, universe=universe)
                            self.emit('child_added')
                        elif pKey == 'Categories':
                            for cKey, cVal in val.iteritems():
                                #category = Category(deserialize=cVal)
                                parent = self.root_category.find_category(id=key)
                                if not parent:
                                    parent = self.root_category.add_subcategory(deserialize=cVal)
                                    self.Categories.update({key:parent.subcategories})
                                category = self.root_category.find_category(id=cKey)
                                if not category:
                                    category = parent.add_subcategory(deserialize=cVal)
                                    #self.Categories[key].update({category.id:category})
                                    self.emit('new_category', type=key, name=category.name, id=category.id, category=category)
                                    self.emit('child_added')
                        elif pKey == 'GlobalAttributes' and key not in self.GlobalAttributes:
                            if key not in self.GlobalAttributes:
                                attribute = Attribute.GlobalAttribute(deserialize=val, root_category=self.root_category.subcategories['attribute'])
                                self.GlobalAttributes.update({key:attribute})
                                self.emit('new_attribute', type='GlobalAttribute', id=key, name=attribute.name)
                                self.emit('child_added')
                        elif pKey == 'DefinedDevices' and key not in self.DefinedDevices:
                            if key not in self.DefinedDevices:
                                d_kwargs = dict(deserialize=val, root_category=self.root_category.subcategories['device'])
                                d_kwargs.update({'GlobalAttributes':self.GlobalAttributes})
                                device = Device.DefinedDevice(**d_kwargs)
                                self.DefinedDevices.update({key:device})
                                self.emit('new_device', base='DefinedDevice', type=device.saved_class_name, 
                                          id=key, name=device.name)
                                self.emit('child_added')
                        elif pKey == 'PatchedDevices' and key not in self.PatchedDevices:
                            if key not in self.PatchedDevices:
                                d_kwargs = dict(osc_parent_node=self.osc_child_nodes['Devices'], 
                                                osc_address=val['attrs'].get('osc_address'))
                                d_kwargs.update({'deserialize':val, 'root_category':self.root_category.subcategories['device']})
                                definition = self.DefinedDevices[val['attrs']['definition_id']]
                                d_kwargs.update({'definition':definition})
                                device = Device.PatchedDevice(**d_kwargs)
                                device.connect('device_num_update', self.on_device_num_update)
                                self.PatchedDevices.update({key:device})
                                self.DevicesByNum.update({device.device_num:device})
                                self._update_attributes(device=device)
                                self.emit('new_device', base='PatchedDevice', type=device.saved_class_name, 
                                          id=key, name=device.name, device=device)
                                self.emit('child_added')
